/** * 文件上传组件 * 基于 Element Plus 的 Upload 组件封装 *
支持拖拽上传、文件类型限制、大小限制等功能 * @component ProUploadFile */
<template>
  <div class="pro-upload-file-box">
    <el-upload
      :class="['pro-upload-file', { 'is-disabled': disabled }]"
      ref="proUploadFileRef"
      v-bind="uploadProps"
      :on-success="handleSuccess"
      :before-upload="handleBeforeUpload"
      :on-exceed="handleExceed"
      :on-error="handleError"
    >
      <slot>
        <el-button v-if="!drag" type="primary" :disabled="disabled">
          <el-icon class="upload-icon"> <Plus /> </el-icon>文件上传
        </el-button>
        <div v-else>
          <el-image
            class="img-upload"
            src="https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/upload.png"
          />
          <div class="upload-guide">
            {{ dragTitle }}
          </div>
          <div v-if="accept" class="upload-support">
            支持扩展名：{{ accept.split(',').join(' ') }}
          </div>
        </div>
      </slot>
      <!-- file插槽 -->
      <template #file>
        <slot name="file" />
      </template>
      <template #tip>
        <div v-if="tip" class="upload-tip">
          {{ tip }}
        </div>
      </template>
    </el-upload>

    <div v-if="!showFileList" class="pro-upload-file-content">
      <div
        v-for="(file, index) in fileList"
        :key="file.uid"
        class="file-list-item"
        @mouseenter="hoverItem = file.uid"
        @mouseleave="hoverItem = ''"
        @click="fileClick(file)"
      >
        <div class="content">
          <div class="file-list-item-left">
            <el-image
              v-if="fileTypeObj(file.name).showFileIcon"
              class="file-icon"
              :src="fileTypeObj(file.name).fileIconUrl"
            />
            <div>
              <div class="file-list-item-name">
                {{ file.name }}
              </div>
              <div class="file-list-item-state">
                {{ file.loadingState ? '上传中' : '上传完成' }}
              </div>
            </div>
          </div>
          <template v-if="!file.loadingState">
            <el-icon
              v-if="hoverItem === file.uid && !disabled"
              class="file-list-item-icon-del"
              @click.stop="handleRemove(file, index)"
            >
              <Close />
            </el-icon>
            <el-icon v-else class="file-list-item-icon">
              <Check />
            </el-icon>
          </template>
        </div>

        <el-progress
          v-if="file.percentage < 100"
          color="rgba(11, 211, 211, 1)"
          :stroke-width="2"
          :show-text="false"
          :percentage="file.percentage"
        />
      </div>
    </div>
  </div>
</template>

<script setup name="ProUploadFile">
  import { ref, computed } from 'vue'
  import { Close, Check, Plus } from '@element-plus/icons-vue'
  import { ElMessage } from 'element-plus'

  const proUploadFileRef = ref(null)
  const hoverItem = ref('')

  // 计算属性
  const fileTypeList = computed(() => {
    if (!props.accept) return []
    return props.accept.split(',').map((item) => item.replace('.', ''))
  })

  const maxSizeInKB = computed(() => {
    if (!props.maxSize) return 0
    return props.sizeUnit === 'MB' ? props.maxSize * 1024 : props.maxSize
  })

  // 组件属性定义
  const props = defineProps({
    /** 上传地址 */
    action: {
      type: String,
      required: true,
      default: ''
    },
    /** 接受的文件类型 */
    accept: {
      type: String,
      default: '.xls,.xlsx'
    },
    /** 文件大小限制 */
    maxSize: {
      type: Number,
      default: 0
    },
    /** 大小单位（KB/MB） */
    sizeUnit: {
      type: String,
      default: 'MB',
      validator: (value) => ['KB', 'MB'].includes(value)
    },
    /** 是否支持拖拽 */
    drag: {
      type: Boolean,
      default: true
    },
    /** 文件数量限制 */
    limit: {
      type: Number,
      default: 0
    },
    /** 是否支持多选 */
    multiple: {
      type: Boolean,
      default: false
    },
    /** 是否显示文件列表 */
    showFileList: {
      type: Boolean,
      default: false
    },
    /** 请求头 */
    headers: {
      type: Object,
      default: () => ({})
    },
    /** 是否禁用 */
    disabled: {
      type: Boolean,
      default: false
    },
    /** 提示信息 */
    tip: {
      type: String,
      default: ''
    },
    /** 拖拽提示文字 */
    dragTitle: {
      type: String,
      default: '点击或将文件拖拽到这里上传'
    }
  })

  // 组件事件定义
  const emit = defineEmits(['success', 'fileClick', 'remove', 'error', 'exceed'])

  // 文件列表双向绑定
  const fileList = defineModel('fileList', {
    type: Array,
    default: () => []
  })

  const uploadProps = computed(() => ({
    action: props.action,
    accept: props.accept,
    limit: props.limit,
    drag: props.drag,
    multiple: props.multiple,
    autoUpload: true,
    showFileList: props.showFileList,
    headers: props.headers,
    fileList: fileList.value,
    disabled: props.disabled
  }))

  // 文件类型映射
  const FILE_TYPE_MAP = {
    pdf: ['pdf'],
    excel: ['xls', 'xlsx', 'et'],
    csv: ['csv'],
    image: ['png', 'jpg', 'jpeg', 'gif', 'bmp'],
    doc: ['doc', 'docx'],
    zip: ['zip']
  }

  /**
   * 通用文件类型判断方法
   * @param {string} fileName - 文件名
   * @param {string} type - 文件类型
   * @returns {boolean} 是否为指定类型
   */
  const isFileType = (fileName, type) => {
    const extension = fileName.split('.').pop().toLowerCase()
    return FILE_TYPE_MAP[type]?.includes(extension) || false
  }

  const fileTypeObj = (fileName) => {
    const iconMap = {
      pdf: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/pdf.png',
      excel: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/exc.png',
      csv: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/csv.png',
      image: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/image.png',
      doc: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/doc.png',
      zip: 'https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/zip.png'
    }
    for (const [type, url] of Object.entries(iconMap)) {
      if (isFileType(fileName, type)) {
        return { showFileIcon: true, fileIconUrl: url }
      }
    }
    return { showFileIcon: false, fileIconUrl: '' }
  }

  /**
   * 移除上传文件
   * @param {Object} file - 文件对象
   * @param {number} index - 文件索引
   */
  const handleRemove = (file, index) => {
    fileList.value.splice(index, 1)
    proUploadFileRef.value.handleRemove(file)
    emit('remove', file)
  }

  /**
   * 上传前校验
   * @param {File} file - 文件对象
   * @returns {boolean} 是否允许上传
   */
  const handleBeforeUpload = (file) => {
    if (!validateFileType(file)) return false
    if (!validateFileSize(file)) return false
    addFileToList(file)
    return true
  }

  /**
   * 验证文件类型
   * @param {File} file - 文件对象
   * @returns {boolean} 是否符合类型要求
   */
  const validateFileType = (file) => {
    if (!props.accept) return true

    const fileType = file.name.split('.').pop()
    if (!fileTypeList.value.includes(fileType)) {
      ElMessage({
        message: `仅支持 ${fileTypeList.value.join('、')} 格式`,
        type: 'warning'
      })
      return false
    }
    return true
  }

  /**
   * 验证文件大小
   * @param {File} file - 文件对象
   * @returns {boolean} 是否符合大小要求
   */
  const validateFileSize = (file) => {
    if (!props.maxSize) return true
    const fileSize = file.size / 1024
    if (fileSize > maxSizeInKB.value) {
      ElMessage({
        message: `大小不能超过 ${props.maxSize}${props.sizeUnit}！`,
        type: 'warning'
      })
      return false
    }
    return true
  }

  /**
   * 创建文件对象
   * @param {File} file - 原始文件对象
   * @returns {Object} 处理后的文件对象
   */
  const createFileObject = (file) => ({
    name: file.name,
    url: '',
    percentage: 0,
    loadingState: 1,
    type: file.type,
    uid: file.uid
  })

  /**
   * 添加文件到列表
   * @param {File} file - 文件对象
   */
  const addFileToList = (file) => {
    const obj = createFileObject(file)

    // 模拟上传进度
    obj.interval = setInterval(() => {
      const index = fileList.value.findIndex((i) => i.uid === obj.uid)
      const targetFile = fileList.value[index]
      if (targetFile?.percentage < 90) {
        targetFile.percentage += 10
      }
    }, 300)

    fileList.value.push(obj)
  }

  /**
   * 处理超出限制
   * @param {File[]} files - 文件列表
   * @param {File[]} uploadFiles - 已上传文件列表
   */
  const handleExceed = (files, uploadFiles) => {
    ElMessage({
      message: `最多只能上传 ${props.limit} 个文件`,
      type: 'warning'
    })
    emit('exceed', files, uploadFiles)
  }

  /**
   * 处理上传成功
   * @param {Object} response - 响应数据
   * @param {Object} uploadFile - 上传文件对象
   * @param {Object[]} uploadFiles - 上传文件列表
   */
  const handleSuccess = (response, uploadFile, uploadFiles) => {
    const index = fileList.value.findIndex((item) => item.uid === uploadFile.uid)
    const targetFile = fileList.value[index]

    if (response.code === 0) {
      const { data } = response
      if (data.fileUrl) targetFile.url = data.fileUrl
      if (data.fileName) targetFile.name = data.fileName
      targetFile.percentage = 100
      targetFile.loadingState = 0
    } else {
      fileList.value.splice(index, 1)
      proUploadFileRef.value.handleRemove(uploadFile)
      ElMessage({
        message: response?.msg || response?.message || '上传失败',
        type: 'error'
      })
    }

    // 清理定时器
    if (targetFile && targetFile.interval) {
      clearInterval(targetFile.interval)
      targetFile.interval = null
    }

    emit('success', response, uploadFile, uploadFiles)
  }

  /**
   * 处理上传错误
   * @param {Error} error - 错误对象
   * @param {Object} uploadFile - 上传文件对象
   * @param {Object[]} uploadFiles - 上传文件列表
   */
  const handleError = (error, uploadFile, uploadFiles) => {
    const index = fileList.value.findIndex((item) => item.uid === uploadFile.uid)
    proUploadFileRef.value.handleRemove(uploadFile)
    if (fileList.value[index] && fileList.value[index].interval) {
      clearInterval(fileList.value[index].interval)
      fileList.value[index].interval = null
    }
    fileList.value.splice(index, 1)
    ElMessage({
      message: '上传失败',
      type: 'error'
    })
    emit('error', error, uploadFile, uploadFiles)
  }

  /**
   * 处理文件点击
   * @param {Object} file - 文件对象
   */
  const fileClick = (file) => {
    if (file.loadingState === 1) {
      ElMessage({
        message: '文件上传中！',
        type: 'warning'
      })
      return
    }
    emit('fileClick', file)
  }
</script>

<style lang="scss" scoped>
  .pro-upload-file-box {
    text-align: left;

    .is-disabled {
      :deep(.el-upload-dragger) {
        cursor: not-allowed;
      }
    }

    .pro-upload-file {
      position: relative;

      .upload-icon {
        margin-right: 4px;
        font-size: 16px;
      }

      .img-upload {
        width: 48px;
        height: 48px;
      }

      .upload-guide {
        font-size: 14px;
        color: rgb(0 5 27 / 85%);
      }

      .upload-support {
        margin-top: 4px;
        font-size: 12px;
        color: rgb(0 5 27 / 45%);
      }
    }

    .upload-tip {
      margin-top: 8px;
      font-size: 12px;
      line-height: 20px;
      color: #909399;
      text-align: left;
    }

    .pro-upload-file-content {
      width: 100%;
      margin-top: 16px;

      .file-list-item {
        width: 100%;
        margin-bottom: 8px;
        cursor: pointer;
        background: #f6f7fb;
        border-radius: 4px;

        .content {
          box-sizing: border-box;
          display: flex;
          align-items: center;
          justify-content: space-between;
          width: 100%;
          padding: 12px 20px;
          background: #f6f7fb;
          border-radius: 4px;
        }
      }

      .file-list-item-left {
        display: flex;
        align-items: center;
        width: calc(100% - 32px);

        .file-icon {
          flex-shrink: 0;
          width: 40px;
          height: 40px;
          margin-right: 12px;
        }
      }

      .file-list-item-name {
        width: 100%;
        overflow: hidden;
        font-size: 14px;
        color: rgb(0 5 27 / 85%);
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .file-list-item-state {
        font-size: 12px;
        color: rgb(0 5 27 / 45%);
      }

      .file-list-item-icon {
        font-size: 16px;
        color: #0bd3d3;
      }

      .file-list-item-icon-del {
        font-size: 16px;
        color: rgb(0 5 27 / 65%);
      }
    }

    :deep(.el-upload-dragger) {
      background: rgb(11 211 211 / 2%);
      border: 1px dashed rgb(11 211 211 / 100%);

      &.is-dragover,
      &:hover {
        background: rgb(11 211 211 / 10%) !important;
        border: 1px dashed rgb(11 211 211 / 100%) !important;
      }
    }

    :deep(.el-progress-bar__outer) {
      border-radius: 4px;
    }
  }
</style>
